using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using WorldGenerators.Scripts.Abstractions.Interfaces;
using WorldGenerators.Scripts.Abstractions.Enums;

namespace WorldGenerators.Scripts
{
    public class WorldGenerator : MonoBehaviour, IGenerator
    {
        private const int MIN_WEIGHT_VALUE = 0;
        private const int MAX_WEIGHT_VALUE = 100;
        private const int MIN_NUMBER_OF_ROTATIONS = 0;
        private const int MAX_NUMBER_OF_ROTATIONS = 4;

        [SerializeField] private float _tileSizeInUnity;
        [SerializeField] private Vector2Int _worldSize;
        [Space]
        [SerializeField] private GameObject[] _tilePrefabs;
        private ITile[] _tiles;

        private ITile[,] _worldMatrix;
        private List<GameObject>[,] _possibleTilePrefabsMatrix;
        private Vector3 _worldOffset;

        private readonly Dictionary<TileSide, Vector2Int> _tileSideOffsets = new() 
        {
            [TileSide.Back] = new Vector2Int(0, -1),
            [TileSide.Left] = new Vector2Int(-1, 0),
            [TileSide.Right] = new Vector2Int(1, 0),
            [TileSide.Front] = new Vector2Int(0, 1)
        };

        private void OnValidate()
        {
            for (int i = 0; i < _tilePrefabs.Length; i++)
                if (_tilePrefabs[i]?.GetComponent<ITile>() == null)
                    _tilePrefabs[i] = null;
        }

        private void Awake()
        {
            _worldMatrix = new ITile[_worldSize.x, _worldSize.y];
            _worldOffset = GetLeftBottomPoint();
            _tiles = _tilePrefabs
                .Select(go => go.GetComponent<ITile>())
                .OrderByDescending(tile => tile.Weight)
                .ToArray();
        }

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
                Generate();
        }

        public void Generate()
        {
            FillTilePrefabsMatrix();
            ClearWorldMatrix();
            for (int x = 0; x < _worldSize.x; x++)
                for (int y = 0; y < _worldSize.y; y++)
                    SpawnTile(x, y);
        }

        private void FillTilePrefabsMatrix()
        {
            _possibleTilePrefabsMatrix = new List<GameObject>[_worldSize.x, _worldSize.y];
            for (int x = 0; x < _worldSize.x; x++)
                for (int y = 0; y < _worldSize.y; y++)
                    _possibleTilePrefabsMatrix[x, y] = _tilePrefabs.ToList();
        }
        private void ClearWorldMatrix()
        {
            foreach (var tile in _worldMatrix)
                GameObject.Destroy(TryGetTileObject(tile));
        }

        private void SpawnTile(int x, int y)
        {
            var tilePrefab = TryGetPossibleTilePrefabByWeight(x, y);
            var position = new Vector3(x, 0, y) * _tileSizeInUnity + _worldOffset;
            var randomNumberOfRotations = GetRandomNumber(
                MIN_NUMBER_OF_ROTATIONS, 
                MAX_NUMBER_OF_ROTATIONS);

            var tile = InstantiateTile(tilePrefab, position, 0);
            _worldMatrix[x, y] = tile;

            TryUpdatePossibleTilePrefabsBySides(x, y);
        }

        private ITile InstantiateTile(
            GameObject tilePrefab, 
            Vector3 position, 
            int numberOfRotations)
        {
            var tileObject = Instantiate(tilePrefab);
            var tileTransform = tileObject.transform;
            
            tileTransform.position = position;
            tileTransform.parent = transform;
            tileObject.name = tilePrefab.name;

            var tile = tileObject.GetComponent<ITile>();
            
            for (int i = 0; i < numberOfRotations; i++)
               tile.Rotate90();

            return tile;
        }

        private bool CheckTileForCompatibility(int x, int y)
        {
            var centralTile = TryGetTile(x, y);
            if (centralTile == null)
                return false;

            foreach (var tileSide in _tileSideOffsets.Keys)
            {
                var possibleTileNames = GetPossibleTilePrefabs(centralTile, tileSide)
                    .Select(prefab => prefab.name);
                
                var tileObject = TryGetTileObject(x, y, tileSide);
                if (tileObject == null || possibleTileNames.Contains(tileObject.name))
                    continue;
                
                return false;
            }
            return true;
        }

        private GameObject TryGetPossibleTilePrefabByWeight(int x, int y)
        {
            var possibleTilePrefabs = TryGetPossibleTilePrefabs(x, y);
            if (possibleTilePrefabs == null)
                return null;

            var numberOfPrefabs = possibleTilePrefabs.Count;
            for (int i = 0, length = numberOfPrefabs - 1; i < length; i++)
            {
                var tilePrefab =  possibleTilePrefabs[i];
                int randmoWight = GetRandomNumber(MIN_WEIGHT_VALUE, MAX_WEIGHT_VALUE + 1);
                if (randmoWight <= tilePrefab.GetComponent<ITile>().Weight)
                    return tilePrefab;
            }
            return possibleTilePrefabs[numberOfPrefabs - 1];
        }

        private GameObject[] GetPossibleTilePrefabs(ITile tile, TileSide tileSide)
        {
            switch (tileSide)
            {
                case TileSide.Right:
                    return tile.PossibleRightTilePrefabs;
                case TileSide.Front:
                    return tile.PossibleFrontTilePrefabs;
                case TileSide.Left:
                    return tile.PossibleLeftTilePrefabs;
                case TileSide.Back:
                    return tile.PossibleBackTilePrefabs;
            }
            return null;
        }

        private List<GameObject> TryGetPossibleTilePrefabs(int x, int y)
        {
            try
            {
                return _possibleTilePrefabsMatrix[x, y];
            }
            catch { }
            return null;
        }
        private List<GameObject> TryGetPossibleTilePrefabs(int x, int y, TileSide tileSide)
        {
            var offset = _tileSideOffsets[tileSide];
            return TryGetPossibleTilePrefabs(x + offset.x, y + offset.y);
        }

        private void TryUpdatePossibleTilePrefabsBySides(int x, int y)
        {
            var tile = TryGetTile(x, y);
            if (tile == null)
                return;

            _possibleTilePrefabsMatrix[x, y].Clear();
            _possibleTilePrefabsMatrix[x, y].Add(TryGetTileObject(x, y));

            foreach (var tileSide in _tileSideOffsets.Keys)
            {
                var possibleTilePrefabs = GetPossibleTilePrefabs(tile, tileSide);
                if (possibleTilePrefabs == null)
                    continue;

                var matrixPossibleTilePrefabs = TryGetPossibleTilePrefabs(x, y, tileSide);
                if (matrixPossibleTilePrefabs == null)
                    continue;

                var matrixPossibleTileNames = matrixPossibleTilePrefabs
                    .Select(prefab => prefab.name);
                var possibleTileNames = possibleTilePrefabs
                    .Select(prefab => prefab.name);

                var intersectingTileNames = matrixPossibleTileNames
                    .Intersect(possibleTileNames);

                var intersectingTilePrefabs = possibleTilePrefabs
                    .Where(prefab => intersectingTileNames.Contains(prefab.name))
                    .ToList();

                var offset = _tileSideOffsets[tileSide];
                _possibleTilePrefabsMatrix[x + offset.x, y + offset.y] = intersectingTilePrefabs;

            }
        }

        private ITile TryGetTile(int x, int y)
        {
            try
            {
                return _worldMatrix[x, y];
            }
            catch { }
            return null;
        }
        private ITile TryGetTile(int x, int y, TileSide tileSide)
        {
            var offset = _tileSideOffsets[tileSide];
            return TryGetTile(x + offset.x, y + offset.y);
        }

        private GameObject TryGetTileObject(ITile tile)
        {
            if (tile == null)
                return null;
            return ((MonoBehaviour)tile).gameObject;
        }
        private GameObject TryGetTileObject(int x, int y)
        {
            var tile = TryGetTile(x, y);
            if (tile == null)
                return null;

            return TryGetTileObject(tile);
        }
        private GameObject TryGetTileObject(int x, int y, TileSide tileSide)
        {
            var offset = _tileSideOffsets[tileSide];
            return TryGetTileObject(x + offset.x, y + offset.y);
        }
        
        private int GetRandomNumber(int startNumber, int endNumber)
        {
            return UnityEngine.Random.Range(startNumber, endNumber);
        }

        private Vector3 GetLeftBottomPoint()
        {
            var point = 
                transform.position + 
                new Vector3(_worldSize.x - 1, 0, _worldSize.y - 1) * _tileSizeInUnity / -2f;
            return point;
        }

        private void OnDrawGizmos()
        {
            Gizmos.color = Color.green;

            var offset = GetLeftBottomPoint();
            for (int x = 0; x < _worldSize.x; x++)
                for (int y = 0; y < _worldSize.y; y++)
                    Gizmos.DrawWireCube(
                        offset + new Vector3(x, 0, y) * _tileSizeInUnity, 
                        Vector3.one * _tileSizeInUnity);
        }
    }
}
